package monitor

import (
	"fmt"
	"io/ioutil"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"
)

// LinuxInfo is for linux platform
type LinuxInfo struct{}

var totalMemSize uint64 = 0
var localCPUNum int = 1

//Init calls before everything
func (li *LinuxInfo) Init() {

	sys := &LinuxInfo{}

	mem := MemInfo{}
	cpus := make([]CPUInfo, 64)

	sys.GetMemInfo(&mem)
	totalMemSize = mem.total
	localCPUNum, _ = sys.GetCPUInfo(CPUAll, cpus)
}

// GetCPUNum returns the number of CPU Cores
func (li *LinuxInfo) GetCPUNum() int {

	return localCPUNum
}

// GetProcessPids return pids of process names
func (li *LinuxInfo) GetProcessPids(names []string) (map[string][]int, error) {

	if len(names) < 1 {
		return nil, fmt.Errorf("not any process names")
	}

	root, err := os.Open("/proc")

	if err != nil {
		return nil, err
	}

	dirs, err := root.Readdirnames(-1)
	root.Close()

	if err != nil {
		return nil, err
	}

	// save int
	expected := make(map[string]bool, len(names))
	for _, v := range names {
		expected[v] = true
	}

	maps := make(map[string][]int, len(names))

	for _, dir := range dirs {
		// not a number, skip
		pid, err := strconv.Atoi(dir)
		if err != nil {
			continue
		}

		// read file name
		buf, err := ioutil.ReadFile(fmt.Sprintf("/proc/%s/comm", dir))
		if err != nil || len(buf) == 0 {
			continue
		}
		// get name
		name := strings.TrimSpace(string(buf))

		_, exists := expected[name]
		if exists {
			maps[name] = append(maps[name], pid)
		}
	}

	return maps, nil
}

func (li *LinuxInfo) parseMemSize(regex string, text []byte) uint64 {

	rx := regexp.MustCompile(regex)
	match := rx.FindAllSubmatch(text, -1)

	if len(match) > 0 {
		s := strings.Fields(strings.TrimSpace(string(match[0][1])))[0]
		a, err := strconv.ParseUint(s, 10, 0)

		if err != nil {
			a = 0
		}
		return a
	}

	return 0
}

// GetMemInfo returns memory usaged infos
func (li *LinuxInfo) GetMemInfo(m *MemInfo) error {

	if m == nil {
		return fmt.Errorf("Empty instance pointer")
	}

	m.stamp = time.Now()
	lines, err := ioutil.ReadFile("/proc/meminfo")

	if err != nil {
		return err
	}

	m.total = li.parseMemSize("MemTotal:(.*)", lines)
	m.free = li.parseMemSize("MemFree:(.*)", lines)
	m.shared = li.parseMemSize("MemShared:(.*)", lines) + li.parseMemSize("Shmem:(.*)", lines)
	m.buffers = li.parseMemSize("Buffers:(.*)", lines)
	m.cached = li.parseMemSize("Cached:(.*)", lines)

	return nil
}

func parseCPUJiffies(head string) uint64 {

	var cpu CPUInfo

	n, err := fmt.Sscanf(head, "cpu %d %d %d %d %d %d %d %d", &cpu.usr, &cpu.nice, &cpu.sys, &cpu.idle,
		&cpu.iowait, &cpu.irq, &cpu.softirq, &cpu.steal)

	if err != nil || n < 8 {
		return 0
	}

	cpu.total = cpu.usr + cpu.nice + cpu.sys + cpu.idle + cpu.iowait + cpu.irq + cpu.softirq + cpu.steal
	return cpu.total
}

// GetCPUInfo returns cpu jiffies
func (li *LinuxInfo) GetCPUInfo(target int, cpus []CPUInfo) (int, error) {

	var i, n int

	stamp := time.Now()
	lines, err := ioutil.ReadFile("/proc/stat")

	if err != nil {
		return 0, err
	}

	ns := strings.Split(string(lines), "\n")

	if len(ns) == 0 {
		return 0, fmt.Errorf("Parsed /proc/stat failed")
	}

	cpu := &cpus[0]
	n, err = fmt.Sscanf(ns[0], "cpu %d %d %d %d %d %d %d %d", &cpu.usr, &cpu.nice, &cpu.sys, &cpu.idle,
		&cpu.iowait, &cpu.irq, &cpu.softirq, &cpu.steal)

	if err != nil {
		return 0, err
	}

	if n < 4 {
		return 0, fmt.Errorf("Parsed /proc/stat failed")
	}

	cpu.total = cpu.usr + cpu.nice + cpu.sys + cpu.idle + cpu.iowait + cpu.irq + cpu.softirq + cpu.steal
	cpu.busy = cpu.total - cpu.idle - cpu.iowait
	cpu.stamp = stamp

	i = 1

	if target == CPUSum {
		return 1, nil
	}

	for _, s := range ns[1:] {
		var cpuid int
		cpu := &cpus[i]

		n, err = fmt.Sscanf(s, "cpu%d %d %d %d %d %d %d %d %d", &cpuid, &cpu.usr, &cpu.nice, &cpu.sys, &cpu.idle,
			&cpu.iowait, &cpu.irq, &cpu.softirq, &cpu.steal)

		if err != nil || n < 5 {
			break
		}

		cpu.total = cpu.usr + cpu.nice + cpu.sys + cpu.idle + cpu.iowait + cpu.irq + cpu.softirq + cpu.steal
		cpu.busy = cpu.total - cpu.idle - cpu.iowait
		cpu.stamp = stamp

		i++
	}

	return i, nil
}

// GetProcessInfo returns process stats
func (li *LinuxInfo) GetProcessInfo(pid int, p *ProcessInfo) error {

	if p == nil {
		return fmt.Errorf("Empty instance pointer")
	}

	lines, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))

	if err != nil {
		return err
	}

	// read cpu stat
	stamp := time.Now()
	cpu, err := ioutil.ReadFile("/proc/stat")

	if err != nil {
		return err
	}

	/*

	   1551 (mongod) S 1 1551 1551 0 -1 // (pid, name, state, ppid, pgid, sid, tty_nr, tty_pgrp)
	   1077936384 11034 0 274 0 4776 12362 0  // (task_flags, min_fit, cmin_fit, maj_fit, cmaj_fit, utime, stime, cutime)
	   0 20 0 23 0 4533 1045458944 18714 //(cstime, priority, nice, num_threads, it_real_value, start_time, vsize, rss)
	   18446744073709551615 1 1 0 0 0 0 8405507 //(rlim, start_code...)
	   6145 1260 0 0 0 17 3 0 0 1280 0 0 0 0 0 0 0 0 0 0
	*/
	str := string(lines)

	//tn := time.Now()

	/*
		// this way is very slow, takes about 6us, but strings.Fields only takes 2-3us
		var n int

		n, err = fmt.Sscanf(str, "%d %s %s %*d %*d %*d %*d %*d %*d %*d %*d %*d %*d %d %d %*d %*d %*d %*d %*d %*d %d %d %d",
			&p.pid, &p.name, &p.state, &p.utime, &p.stime, &p.startTime, &p.vsize, &p.rss)
	*/

	fields := strings.Fields(str)

	if len(fields) < 24 {
		return fmt.Errorf("Parsed process stat file failed, num of fields:%d", len(fields))
	}

	p.pid, _ = strconv.Atoi(fields[0])
	p.name = fields[1]
	p.state = fields[2]
	p.utime, _ = strconv.ParseUint(fields[13], 0, 0) // in jiffies
	p.stime, _ = strconv.ParseUint(fields[14], 0, 0) // in jiffies

	p.startTime, _ = strconv.ParseUint(fields[21], 0, 0) // in jiffies
	p.vsize, _ = strconv.ParseUint(fields[22], 0, 0)     // in bytes
	p.rss, _ = strconv.ParseUint(fields[23], 0, 0)       // in page

	//te := time.Now()
	//fmt.Println(te.Sub(tn))

	p.name = p.name[1 : len(p.name)-1]

	p.vsize = p.vsize >> 10                    //  into kbytes
	p.rss = (p.rss * uint64(osPageSize)) >> 10 // into kbytes

	// parse cpu jiffies
	p.sysCPUTime = parseCPUJiffies(string(cpu))
	p.sysCPUNum = localCPUNum
	p.sysMemSize = totalMemSize
	p.stamp = stamp

	return nil
}
